//  Copyright 2011 Charlie Garrett
//
//  Licensed under the Apache License, Version 2.0 (the "License");
//  you may not use this file except in compliance with the License.
//  You may obtain a copy of the License at
//
//  http://www.apache.org/licenses/LICENSE-2.0
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.
//
//  Lexeme.h
//

#include <iostream>
#include <sstream>
#include <string>

#include "assert.h"

#ifndef LEXEME_H
#define LEXEME_H

// A lexeme is a Prolog source code token read from a C++ input stream

namespace logic {
    using namespace std;

    enum LexemeKind {
        SYMBOL = 1,                   // A constant or variable
        NUMBER = 2,                   // A floating point number
        OPEN_PAREN = 3,
        CLOSE_PAREN = 4,
        OPEN_SQUARE_BRACKET = 5,
        CLOSE_SQUARE_BRACKET = 6,
        COMMA = 7,
        PERIOD = 8,
        LIST_CONS = 9,                // |
        IMPLICATION = 10,             // :-
        QUERY = 11,                   // ?-
        PLUS = 12,                    // +
        MINUS = 13,                   // -
        TIMES = 14,                   // *
        DIVIDE = 15,                  // /
        EQUAL = 16,                   // ==
        NOT_EQUAL = 17,               // \==
        LESS_THAN = 18,               // <
        LESS_OR_EQUAL = 19,           // =<    (weird prolog order)
        GREATER_THAN = 20,            // >
        GREATER_OR_EQUAL = 21,        // >=
        DOUBLY_QUOTED_STRING = 22,    // String within ""
        SINGLY_QUOTED_STRING = 23,    // String within ''
        COMMENT = 24,                 // Single-line comment starting with %
        ILLEGAL = 25
    };

    template <class charT, class traits = char_traits<charT> >
    class Lexeme {
    public:        
        Lexeme(unsigned ln, unsigned col, basic_string<charT> t) :
            line_number(ln), column(col), text(t), kind(ILLEGAL) {}
        
        bool IsKind(LexemeKind kd) {
            return kind == kd;
        }
        
        LexemeKind GetKind() {
            return kind;
        }
        
        basic_string<charT> GetText() {
            return text;
        }
        
        virtual void PrintOn(basic_ostream<charT> &strm) const {
            strm << kind << "<" << text << ", " << line_number << ", " << column << ">";
        }
        
        virtual void ScanFrom(basic_istream<charT> &strm) {
            scan_buffer.str("");

            while(strm.good()) {
                charT ch = strm.peek();
                if (isspace(ch)) {
                    // Consume whitespace and increment counters
                    ScanCharacter(strm, false);
                } else if (ch == traits::eof()) {
                    // Exit scanner loop
                    strm.setstate(ios::failbit);
                    break;
                } else if (ch == '(') {
                    // Consume ( and stop scanning
                    ScanSingleCharacterToken(strm, '(', OPEN_PAREN);
                    break;
                } else if (ch == ')') {
                    // Consume ) and stop scanning
                    ScanSingleCharacterToken(strm, ')', CLOSE_PAREN);
                    break;
                } else if (ch == '[') {
                    // Consume [ and stop scanning
                    ScanSingleCharacterToken(strm, '[', OPEN_SQUARE_BRACKET);
                    break;
                } else if (ch == ']') {
                    // Consume ] and stop scanning
                    ScanSingleCharacterToken(strm, ']', CLOSE_SQUARE_BRACKET);
                    break;
                } else if (ch == ',') {
                    // Consume , and stop scanning
                    ScanSingleCharacterToken(strm, ',', COMMA);
                    break;
                } else if (ch == '.') {
                    // Consume . and stop scanning
                    ScanSingleCharacterToken(strm, '.', PERIOD);
                    break;
                } else if (ch == '|') {
                    // Consume | and stop scanning
                    ScanSingleCharacterToken(strm, '|', LIST_CONS);
                    break;
                } else if (ch == '+') {
                    // Consume + and stop scanning
                    ScanSingleCharacterToken(strm, '+', PLUS);
                    break;
                } else if (ch == '-') {
                    // Consume - and stop scanning
                    ScanSingleCharacterToken(strm, '-', MINUS);
                    break;
                } else if (ch == '*') {
                    // Consume * and stop scanning
                    ScanSingleCharacterToken(strm, '*', TIMES);
                    break;
                } else if (ch == '/') {
                    // Consume / and stop scanning
                    ScanSingleCharacterToken(strm, '/', DIVIDE);
                    break;
                } else if (ch == '<') {
                    // Consume < and stop scanning
                    ScanSingleCharacterToken(strm, '<', LESS_THAN);
                    break;
                } else if (ch == ':') {
                    // Consume :- and stop scanning
                    ScanMultiCharacterToken(strm, string(":-"), IMPLICATION);
                    break;
                } else if (ch == '?') {
                    // Consume ?- and stop scanning
                    ScanMultiCharacterToken(strm, string("?-"), QUERY);
                    break;
                } else if (ch == '\\') {
                    // Consume \== and stop scanning
                    ScanMultiCharacterToken(strm, string("\\=="), NOT_EQUAL);
                    break;
                } else if (ch == '=') {
                    // Options are == and =<
                    MarkPosition();
                    ScanKnownCharacter(strm, '=');
                    charT ch = strm.peek();
                    switch (ch) {
                        case '=':
                            ScanKnownCharacter(strm, '=');
                            kind = EQUAL;
                            text = scan_buffer.str();
                            break;
                        case '<':
                            ScanKnownCharacter(strm, '<');
                            kind = LESS_OR_EQUAL;
                            text = scan_buffer.str();
                            break;
                        default:
                            kind = ILLEGAL;
                            text = scan_buffer.str();
                            break;
                    }
                    break;
                } else if (ch == '>') {
                    // Options are > and >=
                    MarkPosition();
                    ScanKnownCharacter(strm, '>');
                    charT ch = strm.peek();
                    switch (ch) {
                        case '=':
                            ScanKnownCharacter(strm, '=');
                            kind = GREATER_OR_EQUAL;
                            text = scan_buffer.str();
                            break;
                        default:
                            kind = GREATER_THAN;
                            text = scan_buffer.str();
                            break;
                    }
                    break;
                } else if (ch == '\'') {
                    ScanSinglyQuotedString(strm);
                    break;
                } else if (ch == '"') {
                    ScanDoublyQuotedString(strm);
                    break;
                } else if (ch == '%') {
                    ScanComment(strm);
                    break;
                } else if (IsSymbolStartChar(ch)) {
                    ScanSymbol(strm);
                    break;
                } else if (IsNumberStartChar(ch)) {
                    ScanNumber(strm);
                    break;
                } else {
                    MarkPosition();
                    ScanCharacter(strm);
                    kind = ILLEGAL;
                    text = scan_buffer.str();
                    break;
                }
            }
            
            if (strm.fail()) {
                kind = ILLEGAL;
            }
        }
        
        static void ResetCounters() {
            current_line_number = 1;
            current_column = 1;
            scan_buffer.str("");
        }
    private:
        basic_string<charT> text;
        unsigned line_number;
        unsigned column;
        LexemeKind kind;
        
        static unsigned current_line_number;
        static unsigned current_column;
        static basic_ostringstream<charT> scan_buffer;

        // Set the Lexeme's position to the current position.
        void MarkPosition() {
            line_number = current_line_number;
            column = current_column;
        }
        
        // Symbols start with letters or underscores.
        bool IsSymbolStartChar(charT ch) {
            return isalpha(ch) || (ch == '_');
        }
        
        // Symbols continue with letters, digits or underscores.
        bool IsSymbolChar(charT ch) {
            return isalnum(ch) || (ch == '_');
        }
        
        // Numbers start with digits.  Minus signs are parsed separately.
        bool IsNumberStartChar(charT ch) {
            return isdigit(ch);
        }
        
        // Numbers continue with digits or decimal points.
        bool IsNumberChar(charT ch) {
            return isdigit(ch) || (ch == '.');
        }
        
        // Scan one character and return it, updating the position counters
        // and scan buffer.
        charT ScanCharacter(basic_istream<charT> &strm, bool remember = true) {
            charT read_ch = strm.get();
            
            if (remember) {
                scan_buffer << read_ch;
            }
            
            if (read_ch == '\n') {
                current_line_number++;
                current_column = 1;
            } else {
                current_column++;
            }
            
            return read_ch;
        }
        
        // Scan one known character.
        void ScanKnownCharacter(basic_istream<charT> &strm, const charT known_ch) {
            charT read_ch = ScanCharacter(strm);
            assert(read_ch == known_ch);
        }
        
        // Scan all characters up to a particular sentinel, if possible.
        void ScanUpToCharacter(basic_istream<charT> &strm, const charT sentinel, bool include = true) {
            while (!strm.fail()) {
                if (ScanCharacter(strm) == sentinel) {
                    return;
                }
            }
        }
        
        void ScanSingleCharacterToken(basic_istream<charT> &strm, const charT known_ch, const LexemeKind kd) {
            MarkPosition();
            ScanKnownCharacter(strm, known_ch);
            kind = kd;
            text = scan_buffer.str();
        }

        void ScanMultiCharacterToken(basic_istream<charT> &strm, const string &known_str, const LexemeKind kd) {
            assert(!known_str.empty());
            MarkPosition();
            ScanKnownCharacter(strm, known_str.at(0));
            
            for (int n = 1; n < known_str.length(); n++) {
                charT ch = strm.peek();
                if (ch == known_str.at(n)) {
                    ScanKnownCharacter(strm, ch);
                } else {
                    // Some character failed to match.
                    kind = ILLEGAL;
                    text = scan_buffer.str();
                    return;
                }
            }
            
            // All characters matched.
            kind = kd;
            text = scan_buffer.str();
        }
        
        void ScanSinglyQuotedString(basic_istream<charT> &strm) {
            MarkPosition();
            
            ScanKnownCharacter(strm, '\'');
            
            ScanUpToCharacter(strm, '\'');
            
            if (!strm.fail()) {
                kind = SINGLY_QUOTED_STRING;
                text = scan_buffer.str();
            } else {
                kind = ILLEGAL;
            }
        }
        
        void ScanDoublyQuotedString(basic_istream<charT> &strm) {
            MarkPosition();
            
            ScanKnownCharacter(strm, '"');
            
            ScanUpToCharacter(strm, '"');
            
            if (!strm.fail()) {
                kind = DOUBLY_QUOTED_STRING;
                text = scan_buffer.str();
            } else {
                kind = ILLEGAL;
            }
        }
        
        void ScanComment(basic_istream<charT> &strm) {
            MarkPosition();
            
            ScanKnownCharacter(strm, '%');
            
            ScanUpToCharacter(strm, '\n');
            
            if (!strm.fail()) {
                kind = COMMENT;
                text = scan_buffer.str().substr(0, scan_buffer.str().length() - 1);
            } else {
                kind = ILLEGAL;
            }
        }
        
        void ScanSymbol(basic_istream<charT> &strm) {
            MarkPosition();
            
            charT ch = ScanCharacter(strm);
            assert(IsSymbolStartChar(ch));
            
            while (!strm.fail()) {
                ch = strm.peek();
                if (IsSymbolChar(ch)) {
                    ScanCharacter(strm);
                } else {
                    kind = SYMBOL;
                    text = scan_buffer.str();
                    break;
                }
            }
            
            if (strm.fail()) {
                kind = ILLEGAL;
            }
        }
        
        void ScanNumber(basic_istream<charT> &strm) {
            MarkPosition();
            
            charT ch = ScanCharacter(strm);
            assert(IsNumberStartChar(ch));
            
            while (!strm.fail()) {
                ch = strm.peek();
                if (IsNumberChar(ch)) {
                    ScanCharacter(strm);
                } else {
                    kind = NUMBER;
                    text = scan_buffer.str();
                    break;
                }
            }
        }
    };
    
    // Print a LexemeKind on an output stream.
    template <class charT, class traits>
    basic_ostream<charT, traits>& operator << (basic_ostream<charT, traits> &strm, const LexemeKind kind) {
        switch (kind) {
            case SYMBOL:
                strm << "SYMBOL";
                break;
            case NUMBER:
                strm << "NUMBER";
                break;
            case OPEN_PAREN:
                strm << "OPEN_PAREN";
                break;
            case CLOSE_PAREN:
                strm << "CLOSE_PAREN";
                break;
            case OPEN_SQUARE_BRACKET:
                strm << "OPEN_SQUARE_BRACKET";
                break;
            case CLOSE_SQUARE_BRACKET:
                strm << "CLOSE_SQUARE_BRACKET";
                break;
            case COMMA:
                strm << "COMMA";
                break;
            case PERIOD:
                strm << "PERIOD";
                break;
            case LIST_CONS:
                strm << "LIST_CONS";
                break;
            case IMPLICATION:
                strm << "IMPLICATION";
                break;
            case QUERY:
                strm << "QUERY";
                break;
            case PLUS:
                strm << "PLUS";
                break;
            case MINUS:
                strm << "MINUS";
                break;
            case TIMES:
                strm << "TIMES";
                break;
            case DIVIDE:
                strm << "DIVIDE";
                break;
            case EQUAL:
                strm << "EQUAL";
                break;
            case NOT_EQUAL:
                strm << "NOT_EQUAL";
                break;
            case LESS_THAN:
                strm << "LESS_THAN";
                break;
            case LESS_OR_EQUAL:
                strm << "LESS_OR_EQUAL";
                break;
            case GREATER_THAN:
                strm << "GREATER_THAN";
                break;
            case GREATER_OR_EQUAL:
                strm << "GREATER_OR_EQUAL";
                break;
            case DOUBLY_QUOTED_STRING:
                strm << "DOUBLY_QUOTED_STRING";
                break;
            case SINGLY_QUOTED_STRING:
                strm << "SINGLY_QUOTED_STRING";
                break;
            case COMMENT:
                strm << "COMMENT";
                break;
            case ILLEGAL:
                strm << "ILLEGAL";
                break;
        }
        return strm;
    }
    
    // Print a Lexeme on an output stream.  Make the function generic across
    // different types of output streams.
    template <class charT, class traits>
    basic_ostream<charT, traits>& operator << (basic_ostream<charT, traits> &strm, const Lexeme<charT> &l) {
        l.PrintOn(strm);
        return strm;
    }
    
    // Parse a Lexeme from an input stream.  Make the function generic across
    // different types of input streams.
    template <class charT, class traits>
    basic_istream<charT, traits>& operator >> (basic_istream<charT, traits> &strm, Lexeme<charT> &l) {
        l.ScanFrom(strm);
        return strm;
    }

    template <class charT, class traits> basic_ostringstream<charT> Lexeme<charT, traits>::scan_buffer;               
    template <class charT, class traits> unsigned Lexeme<charT, traits>::current_line_number;
    template <class charT, class traits> unsigned Lexeme<charT, traits>::current_column;
}  // namespace logic

#endif  // LEXEME_H


